Micron Document
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
| SparkN0de-git | SparkN0de |
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------


Commit 6bc49c435589926f650fb5c564037bcff7e973a7


Parents : aaa2539
Author : Ivan <ivan@quad4.io>
Signature : Signature validation error
Date : 2026-05-08T23:50:55-05:00

feat(tests): add tests for contacts and favourites import/export functionality, including deduplication and icon handling

Changes
Diff

diff --git a/tests/backend/test_contacts_export_import.py b/tests/backend/test_contacts_export_import.py
index fd8fb7e6..c2e2f5b2 100644
--- a/tests/backend/test_contacts_export_import.py
+++ b/tests/backend/test_contacts_export_import.py
@@ -73,6 +73,9 @@ async def test_contacts_export_with_data(mock_rns_minimal, temp_dir):
)
app.database.contacts.add_contact("Alice", "a" * 32, lxmf_address="b" * 32)
app.database.contacts.add_contact("Bob", "c" * 32)
+ app.database.misc.update_lxmf_user_icon(
+ "a" * 32, "account", "#FFFFFF", "#000000"
+ )
handler = None
for route in app.get_routes():
@@ -93,8 +96,13 @@ async def test_contacts_export_with_data(mock_rns_minimal, temp_dir):
assert names == {"Alice", "Bob"}
for c in data["contacts"]:
assert "id" not in c
- assert "created_at" not in c
- assert "updated_at" not in c
+ assert "created_at" in c
+ assert "updated_at" in c
+ alice = next(c for c in data["contacts"] if c["name"] == "Alice")
+ assert "lxmf_icon" in alice
+ assert alice["lxmf_icon"]["icon_name"] == "account"
+ bob = next(c for c in data["contacts"] if c["name"] == "Bob")
+ assert "lxmf_icon" not in bob
@pytest.mark.asyncio
@@ -193,3 +201,41 @@ async def test_contacts_import_rejects_non_array(mock_rns_minimal, temp_dir):
request.json = AsyncMock(return_value={"contacts": "not an array"})
response = await handler(request)
assert response.status == 400
+
+
+@pytest.mark.asyncio
+async def test_contacts_import_deduplicates(mock_rns_minimal, temp_dir):
+ with patch("meshchatx.meshchat.generate_ssl_certificate"):
+ app = ReticulumMeshChat(
+ identity=mock_rns_minimal,
+ storage_dir=temp_dir,
+ reticulum_config_dir=temp_dir,
+ )
+ handler = None
+ for route in app.get_routes():
+ if (
+ route.path == "/api/v1/telephone/contacts/import"
+ and route.method == "POST"
+ ):
+ handler = route.handler
+ break
+ assert handler is not None
+
+ request = MagicMock()
+ request.json = AsyncMock(
+ return_value={
+ "contacts": [
+ {"name": "First", "remote_identity_hash": "a" * 32},
+ {"name": "Second", "remote_identity_hash": "a" * 32},
+ {"name": "Third", "remote_identity_hash": "b" * 32},
+ ],
+ },
+ )
+ response = await handler(request)
+ data = json.loads(response.body)
+ assert data["added"] == 2
+ assert data["skipped"] == 0
+ rows = app.database.contacts.get_contacts(limit=10)
+ assert len(rows) == 2
+ names = {r["name"] for r in rows}
+ assert names == {"Second", "Third"}

diff --git a/tests/backend/test_favourites_import.py b/tests/backend/test_favourites_import.py
new file mode 100644
index 00000000..c6a1ac04
--- /dev/null
+++ b/tests/backend/test_favourites_import.py
@@ -0,0 +1,183 @@
+# SPDX-License-Identifier: 0BSD
+
+import json
+import shutil
+import tempfile
+from unittest.mock import AsyncMock, MagicMock, patch
+
+import pytest
+import RNS
+
+from meshchatx.meshchat import ReticulumMeshChat
+
+
+@pytest.fixture
+def temp_dir():
+ dir_path = tempfile.mkdtemp()
+ yield dir_path
+ shutil.rmtree(dir_path)
+
+
+@pytest.fixture
+def mock_rns_minimal():
+ with (
+ patch("RNS.Reticulum") as mock_rns,
+ patch("RNS.Transport"),
+ patch("LXMF.LXMRouter"),
+ patch("meshchatx.meshchat.get_file_path", return_value="/tmp/mock_path"),
+ ):
+ mock_rns_instance = mock_rns.return_value
+ mock_rns_instance.configpath = "/tmp/mock_config"
+ mock_rns_instance.is_connected_to_shared_instance = False
+ mock_rns_instance.transport_enabled.return_value = True
+
+ mock_id = MagicMock(spec=RNS.Identity)
+ mock_id.hash = b"test_hash_32_bytes_long_01234567"
+ mock_id.hexhash = mock_id.hash.hex()
+ mock_id.get_private_key.return_value = b"test_private_key"
+ yield mock_id
+
+
+@pytest.mark.asyncio
+async def test_favourites_import(mock_rns_minimal, temp_dir):
+ with patch("meshchatx.meshchat.generate_ssl_certificate"):
+ app = ReticulumMeshChat(
+ identity=mock_rns_minimal,
+ storage_dir=temp_dir,
+ reticulum_config_dir=temp_dir,
+ )
+ handler = None
+ for route in app.get_routes():
+ if route.path == "/api/v1/favourites/import" and route.method == "POST":
+ handler = route.handler
+ break
+ assert handler is not None
+
+ request = MagicMock()
+ request.json = AsyncMock(
+ return_value={
+ "favourites": [
+ {
+ "destination_hash": "a" * 32,
+ "display_name": "Node A",
+ "aspect": "nomadnetwork.node",
+ },
+ {
+ "destination_hash": "b" * 32,
+ "display_name": "Node B",
+ "aspect": "nomadnetwork.node",
+ },
+ ],
+ },
+ )
+ response = await handler(request)
+ data = json.loads(response.body)
+ assert data["imported"] == 2
+ assert data["skipped"] == 0
+
+ rows = app.database.announces.get_favourites(aspect="nomadnetwork.node")
+ assert len(rows) == 2
+
+
+@pytest.mark.asyncio
+async def test_favourites_import_skips_invalid(mock_rns_minimal, temp_dir):
+ with patch("meshchatx.meshchat.generate_ssl_certificate"):
+ app = ReticulumMeshChat(
+ identity=mock_rns_minimal,
+ storage_dir=temp_dir,
+ reticulum_config_dir=temp_dir,
+ )
+ handler = None
+ for route in app.get_routes():
+ if route.path == "/api/v1/favourites/import" and route.method == "POST":
+ handler = route.handler
+ break
+ assert handler is not None
+
+ request = MagicMock()
+ request.json = AsyncMock(
+ return_value={
+ "favourites": [
+ {
+ "destination_hash": "a" * 32,
+ "display_name": "Node A",
+ "aspect": "nomadnetwork.node",
+ },
+ {"display_name": "Missing hash"},
+ {"destination_hash": "b" * 32, "aspect": None},
+ ],
+ },
+ )
+ response = await handler(request)
+ data = json.loads(response.body)
+ assert data["imported"] == 1
+ assert data["skipped"] == 2
+
+
+@pytest.mark.asyncio
+async def test_favourites_import_deduplicates(mock_rns_minimal, temp_dir):
+ with patch("meshchatx.meshchat.generate_ssl_certificate"):
+ app = ReticulumMeshChat(
+ identity=mock_rns_minimal,
+ storage_dir=temp_dir,
+ reticulum_config_dir=temp_dir,
+ )
+ handler = None
+ for route in app.get_routes():
+ if route.path == "/api/v1/favourites/import" and route.method == "POST":
+ handler = route.handler
+ break
+ assert handler is not None
+
+ request = MagicMock()
+ request.json = AsyncMock(
+ return_value={
+ "favourites": [
+ {
+ "destination_hash": "a" * 32,
+ "display_name": "First",
+ "aspect": "nomadnetwork.node",
+ },
+ {
+ "destination_hash": "a" * 32,
+ "display_name": "Second",
+ "aspect": "nomadnetwork.node",
+ },
+ {
+ "destination_hash": "b" * 32,
+ "display_name": "Node B",
+ "aspect": "nomadnetwork.node",
+ },
+ ],
+ },
+ )
+ response = await handler(request)
+ data = json.loads(response.body)
+ assert data["imported"] == 2
+ assert data["skipped"] == 0
+
+ rows = app.database.announces.get_favourites(aspect="nomadnetwork.node")
+ assert len(rows) == 2
+ names = {r["display_name"] for r in rows}
+ assert names == {"Second", "Node B"}
+
+
+@pytest.mark.asyncio
+async def test_favourites_import_rejects_non_array(mock_rns_minimal, temp_dir):
+ with patch("meshchatx.meshchat.generate_ssl_certificate"):
+ app = ReticulumMeshChat(
+ identity=mock_rns_minimal,
+ storage_dir=temp_dir,
+ reticulum_config_dir=temp_dir,
+ )
+ handler = None
+ for route in app.get_routes():
+ if route.path == "/api/v1/favourites/import" and route.method == "POST":
+ handler = route.handler
+ break
+ assert handler is not None
+
+ request = MagicMock()
+ request.json = AsyncMock(return_value={"favourites": "not an array"})
+ response = await handler(request)
+ assert response.status == 400

diff --git a/tests/backend/test_http_auth_security.py b/tests/backend/test_http_auth_security.py
index 1f9deda9..cc18e37e 100644
--- a/tests/backend/test_http_auth_security.py
+++ b/tests/backend/test_http_auth_security.py
@@ -157,3 +157,33 @@ def test_auth_login_fuzz_never_500(mock_app, body):
assert r.status != 500
asyncio.run(run())
+
+
+def test_reset_password_clears_hash_when_set(mock_app):
+ mock_app.config.auth_enabled.set(True)
+ h = bcrypt.hashpw(b"old-password", bcrypt.gensalt()).decode("utf-8")
+ mock_app.config.auth_password_hash.set(h)
+ assert mock_app.reset_password() is True
+ assert mock_app.config.auth_password_hash.get() is None
+
+
+def test_reset_password_no_op_when_no_hash(mock_app):
+ mock_app.config.auth_password_hash.set(None)
+ assert mock_app.reset_password() is False
+ assert mock_app.config.auth_password_hash.get() is None
+
+
+@pytest.mark.asyncio
+@pytest.mark.usefixtures("require_loopback_tcp")
+async def test_reset_password_exposes_setup_screen(mock_app):
+ mock_app.config.auth_enabled.set(True)
+ h = bcrypt.hashpw(b"old-password", bcrypt.gensalt()).decode("utf-8")
+ mock_app.config.auth_password_hash.set(h)
+ assert mock_app.reset_password() is True
+
+ aio_app = _make_aio_app(mock_app, use_https=False)
+ async with TestClient(TestServer(aio_app)) as client:
+ status = await client.get("/api/v1/auth/status")
+ assert status.status == 200
+ body = await status.json()
+ assert body["password_set"] is False

diff --git a/tests/backend/test_messages_export.py b/tests/backend/test_messages_export.py
new file mode 100644
index 00000000..2d0aefaa
--- /dev/null
+++ b/tests/backend/test_messages_export.py
@@ -0,0 +1,186 @@
+# SPDX-License-Identifier: 0BSD
+
+import json
+import shutil
+import tempfile
+from unittest.mock import MagicMock, patch
+
+import pytest
+import RNS
+
+from meshchatx.meshchat import ReticulumMeshChat
+
+
+@pytest.fixture
+def temp_dir():
+ dir_path = tempfile.mkdtemp()
+ yield dir_path
+ shutil.rmtree(dir_path)
+
+
+@pytest.fixture
+def mock_rns_minimal():
+ with (
+ patch("RNS.Reticulum") as mock_rns,
+ patch("RNS.Transport"),
+ patch("LXMF.LXMRouter"),
+ patch("meshchatx.meshchat.get_file_path", return_value="/tmp/mock_path"),
+ ):
+ mock_rns_instance = mock_rns.return_value
+ mock_rns_instance.configpath = "/tmp/mock_config"
+ mock_rns_instance.is_connected_to_shared_instance = False
+ mock_rns_instance.transport_enabled.return_value = True
+
+ mock_id = MagicMock(spec=RNS.Identity)
+ mock_id.hash = b"test_hash_32_bytes_long_01234567"
+ mock_id.hexhash = mock_id.hash.hex()
+ mock_id.get_private_key.return_value = b"test_private_key"
+ yield mock_id
+
+
+@pytest.mark.asyncio
+async def test_messages_export_with_icons(mock_rns_minimal, temp_dir):
+ with patch("meshchatx.meshchat.generate_ssl_certificate"):
+ app = ReticulumMeshChat(
+ identity=mock_rns_minimal,
+ storage_dir=temp_dir,
+ reticulum_config_dir=temp_dir,
+ )
+ app.database.messages.upsert_lxmf_message(
+ {
+ "hash": "msg1",
+ "source_hash": "peer1",
+ "destination_hash": "local",
+ "peer_hash": "peer1",
+ "state": "delivered",
+ "progress": 1.0,
+ "is_incoming": 1,
+ "method": "delivery",
+ "delivery_attempts": 0,
+ "next_delivery_attempt_at": None,
+ "title": None,
+ "content": "Hello",
+ "fields": None,
+ "timestamp": 1000.0,
+ "rssi": None,
+ "snr": None,
+ "quality": None,
+ "is_spam": 0,
+ "reply_to_hash": None,
+ "attachments_stripped": None,
+ "path_hops_at_send": None,
+ "path_interface_at_send": None,
+ "path_finding_measure": None,
+ "path_row_hash_hex": None,
+ }
+ )
+ app.database.messages.upsert_lxmf_message(
+ {
+ "hash": "msg2",
+ "source_hash": "local",
+ "destination_hash": "peer2",
+ "peer_hash": "peer2",
+ "state": "delivered",
+ "progress": 1.0,
+ "is_incoming": 0,
+ "method": "delivery",
+ "delivery_attempts": 0,
+ "next_delivery_attempt_at": None,
+ "title": None,
+ "content": "World",
+ "fields": None,
+ "timestamp": 2000.0,
+ "rssi": None,
+ "snr": None,
+ "quality": None,
+ "is_spam": 0,
+ "reply_to_hash": None,
+ "attachments_stripped": None,
+ "path_hops_at_send": None,
+ "path_interface_at_send": None,
+ "path_finding_measure": None,
+ "path_row_hash_hex": None,
+ }
+ )
+ app.database.misc.update_lxmf_user_icon(
+ "peer1", "account", "#FFFFFF", "#000000"
+ )
+ app.database.misc.update_lxmf_user_icon("peer2", "robot", "#000000", "#FFFFFF")
+
+ handler = None
+ for route in app.get_routes():
+ if (
+ route.path == "/api/v1/maintenance/messages/export"
+ and route.method == "GET"
+ ):
+ handler = route.handler
+ break
+ assert handler is not None
+
+ request = MagicMock()
+ response = await handler(request)
+ data = json.loads(response.body)
+ assert "messages" in data
+ assert len(data["messages"]) == 2
+
+ msg1 = next(m for m in data["messages"] if m["hash"] == "msg1")
+ assert "lxmf_icon" in msg1
+ assert msg1["lxmf_icon"]["icon_name"] == "account"
+
+ msg2 = next(m for m in data["messages"] if m["hash"] == "msg2")
+ assert "lxmf_icon" in msg2
+ assert msg2["lxmf_icon"]["icon_name"] == "robot"
+
+
+@pytest.mark.asyncio
+async def test_messages_export_without_icons(mock_rns_minimal, temp_dir):
+ with patch("meshchatx.meshchat.generate_ssl_certificate"):
+ app = ReticulumMeshChat(
+ identity=mock_rns_minimal,
+ storage_dir=temp_dir,
+ reticulum_config_dir=temp_dir,
+ )
+ app.database.messages.upsert_lxmf_message(
+ {
+ "hash": "msg1",
+ "source_hash": "peer1",
+ "destination_hash": "local",
+ "peer_hash": "peer1",
+ "state": "delivered",
+ "progress": 1.0,
+ "is_incoming": 1,
+ "method": "delivery",
+ "delivery_attempts": 0,
+ "next_delivery_attempt_at": None,
+ "title": None,
+ "content": "Hello",
+ "fields": None,
+ "timestamp": 1000.0,
+ "rssi": None,
+ "snr": None,
+ "quality": None,
+ "is_spam": 0,
+ "reply_to_hash": None,
+ "attachments_stripped": None,
+ "path_hops_at_send": None,
+ "path_interface_at_send": None,
+ "path_finding_measure": None,
+ "path_row_hash_hex": None,
+ }
+ )
+
+ handler = None
+ for route in app.get_routes():
+ if (
+ route.path == "/api/v1/maintenance/messages/export"
+ and route.method == "GET"
+ ):
+ handler = route.handler
+ break
+ assert handler is not None
+
+ request = MagicMock()
+ response = await handler(request)
+ data = json.loads(response.body)
+ assert len(data["messages"]) == 1
+ assert "lxmf_icon" not in data["messages"][0]


──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────